/*
 * Wazuh vulnerability scanner - Policy Manager
 * Copyright (C) 2015, Wazuh Inc.
 * March 25, 2023.
 *
 * This program is free software; you can redistribute it
 * and/or modify it under the terms of the GNU General Public
 * License (version 2) as published by the FSF - Free Software
 * Foundation.
 */

#ifndef _POLICY_MANAGER_HPP
#define _POLICY_MANAGER_HPP

#include "loggerHelper.h"
#include "observer.hpp"
#include "routerSubscriber.hpp"
#include "singleton.hpp"
#include "stringHelper.h"
#include "vulnerabilityScanner.hpp"
#include <external/nlohmann/json.hpp>
#include <functional>
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>

constexpr auto UNKNOWN_VALUE {" "};
constexpr auto STATES_VD_INDEX_NAME_PREFIX {"wazuh-states-vulnerabilities-"};
constexpr auto DEFAULT_TRANSLATION_LRU_SIZE {2048};
constexpr auto DEFAULT_OSDATA_LRU_SIZE {1000};
constexpr auto DEFAULT_REMEDIATION_LRU_SIZE {2048};
const static std::string UPDATER_PATH {"queue/vd_updater"};
constexpr auto MANAGER_SCAN_DISABLED {1};
constexpr auto MANAGER_SCAN_ENABLED {0};

/**
 * @brief PolicyManager class.
 *
 */
class PolicyManager final : public Singleton<PolicyManager>
{
private:
    std::unique_ptr<Subject<nlohmann::json&>> m_subject;
    nlohmann::json m_configuration;
    std::unique_ptr<RouterSubscriber> m_policyChangeSubscription;

    /**
     * @brief Set the default policy.
     * This method is used to set the default policy, if the policy is not set in the input configuration.
     *
     * @param configuration The configuration to set the default policy.
     *
     * @return The configuration merged with the default policy.
     */
    nlohmann::json setDefaultPolicy(const nlohmann::json& configuration) const
    {
        nlohmann::json newPolicy = configuration;
        // Set default policy
        if (newPolicy.contains("indexer"))
        {
            if (!newPolicy.at("indexer").contains("hosts"))
            {
                newPolicy["indexer"]["hosts"] = nlohmann::json::array();
                newPolicy["indexer"]["hosts"].push_back("http://localhost:9200");
            }

            if (!newPolicy.at("indexer").contains("username"))
            {
                newPolicy["indexer"]["username"] = "";
            }

            if (!newPolicy.at("indexer").contains("password"))
            {
                newPolicy["indexer"]["password"] = "";
            }

            if (!newPolicy.at("indexer").contains("ssl"))
            {
                newPolicy["indexer"]["ssl"] = nlohmann::json::object();
                newPolicy["indexer"]["ssl"]["certificate_authorities"] = nlohmann::json::array();
                newPolicy["indexer"]["ssl"]["certificate"] = "";
                newPolicy["indexer"]["ssl"]["key"] = "";
            }
            else
            {
                if (!newPolicy.at("indexer").at("ssl").contains("certificate_authorities"))
                {
                    newPolicy["indexer"]["ssl"]["certificate_authorities"] = nlohmann::json::array();
                }

                if (!newPolicy.at("indexer").at("ssl").contains("certificate"))
                {
                    newPolicy["indexer"]["ssl"]["certificate"] = "";
                }

                if (!newPolicy.at("indexer").at("ssl").contains("key"))
                {
                    newPolicy["indexer"]["ssl"]["key"] = "";
                }
            }
        }
        else
        {
            newPolicy["indexer"] = nlohmann::json::object();
            newPolicy["indexer"]["enabled"] = "no";
            newPolicy["indexer"]["hosts"] = nlohmann::json::array();
            newPolicy["indexer"]["username"] = "";
            newPolicy["indexer"]["password"] = "";
            newPolicy["indexer"]["ssl"] = nlohmann::json::object();
            newPolicy["indexer"]["ssl"]["certificate_authorities"] = nlohmann::json::array();
            newPolicy["indexer"]["ssl"]["certificate"] = "";
            newPolicy["indexer"]["ssl"]["key"] = "";
        }
        newPolicy["indexer"]["name"] =
            Utils::toLowerCase(STATES_VD_INDEX_NAME_PREFIX + newPolicy.at("clusterName").get_ref<const std::string&>());

        if (!newPolicy.at("vulnerability-detection").contains("feed-update-interval"))
        {
            newPolicy["vulnerability-detection"]["feed-update-interval"] = "60m";
        }

        if (Utils::parseStrToTime(newPolicy.at("vulnerability-detection").at("feed-update-interval")) <
            Utils::parseStrToTime("60m"))
        {
            logWarn(WM_VULNSCAN_LOGTAG,
                    "The 'feed-update-interval' option at module 'vulnerability-detection' must be at least 1 "
                    "hour. Automatically set to 60 minutes.");
            newPolicy["vulnerability-detection"]["feed-update-interval"] = "60m";
        }

        if (!newPolicy.contains("updater"))
        {
            newPolicy["updater"] = nlohmann::json::object();
            newPolicy["updater"]["interval"] =
                Utils::parseStrToTime(newPolicy.at("vulnerability-detection").at("feed-update-interval"));
            newPolicy["updater"]["ondemand"] = true;
            newPolicy["updater"]["topicName"] = "vulnerability_feed_manager";
            newPolicy["updater"]["configData"] = nlohmann::json::object();
            newPolicy["updater"]["configData"]["consumerName"] = "Wazuh VulnerabilityDetector";

            if (newPolicy.at("vulnerability-detection").contains("offline-url"))
            {
                newPolicy["updater"]["configData"]["contentSource"] = "offline";
                newPolicy["updater"]["configData"]["compressionType"] = "raw";
                newPolicy["updater"]["configData"]["versionedContent"] = "offline";
                newPolicy["updater"]["configData"]["deleteDownloadedContent"] = true;
                newPolicy["updater"]["configData"]["outputFolder"] = UPDATER_PATH + "/tmp";
                newPolicy["updater"]["configData"]["databasePath"] = UPDATER_PATH + "/rocksdb";
                newPolicy["updater"]["configData"]["url"] = newPolicy.at("vulnerability-detection").at("offline-url");
                newPolicy["updater"]["configData"]["offset"] = 0;
            }
            else
            {
                newPolicy["updater"]["configData"]["contentSource"] = "cti-offset";
                newPolicy["updater"]["configData"]["compressionType"] = "raw";
                newPolicy["updater"]["configData"]["versionedContent"] = "cti-api";
                newPolicy["updater"]["configData"]["deleteDownloadedContent"] = true;
                newPolicy["updater"]["configData"]["outputFolder"] = UPDATER_PATH + "/tmp";
                newPolicy["updater"]["configData"]["contentFileName"] = "api_file.json";
                newPolicy["updater"]["configData"]["databasePath"] = UPDATER_PATH + "/rocksdb";
                newPolicy["updater"]["configData"]["url"] = newPolicy.at("vulnerability-detection").at("cti-url");
                newPolicy["updater"]["configData"]["offset"] = 0;
            }
        }

        if (!newPolicy.contains("translationLRUSize"))
        {
            newPolicy["translationLRUSize"] = DEFAULT_TRANSLATION_LRU_SIZE;
        }

        if (!newPolicy.contains("osdataLRUSize"))
        {
            newPolicy["osdataLRUSize"] = DEFAULT_OSDATA_LRU_SIZE;
        }

        if (!newPolicy.contains("remediationLRUSize"))
        {
            newPolicy["remediationLRUSize"] = DEFAULT_REMEDIATION_LRU_SIZE;
        }

        if (!newPolicy.contains("managerDisabledScan"))
        {
            newPolicy["managerDisabledScan"] = MANAGER_SCAN_ENABLED;
        }

        if (!newPolicy.contains("clusterNodeName"))
        {
            newPolicy["clusterNodeName"] = UNKNOWN_VALUE;
        }

        if (!newPolicy.contains("clusterName"))
        {
            newPolicy["clusterName"] = UNKNOWN_VALUE;
        }

        return newPolicy;
    }

    // LCOV_EXCL_START
    /**
     * @brief This is part of the Observer pattern, and is used to notify the observer of a change.
     *
     * @param data A reference to a JSON object containing the data to be passed to the observer.
     *
     * @note The argument have the configuration data.
     */
    void call(nlohmann::json& data)
    {
        m_subject->setData(data);
    }
    // LCOV_EXCL_STOP

    /**
     * @brief Validates and configures the vulnerability detection based on the provided JSON object.
     *
     * This function takes a JSON object as input, which is expected to contain configuration
     * information for vulnerability detection. It validates the JSON object to ensure it contains the
     * required fields and has valid values. If the validation passes, no exception is thrown.
     *
     * @param vdObj A constant reference to a JSON object representing the vulnerability detection's configuration.
     *
     * @note This function assumes that the provided JSON object follows a specific format.
     * @note If validation fails, this function throws std::runtime exception.
     */
    void validateVulnerabilityDetectionConfiguration(const nlohmann::json& vdObj) const
    {
        if (!vdObj.contains("enabled"))
        {
            throw std::runtime_error("Missing enabled field.");
        }

        if (!vdObj.contains("index-status"))
        {
            throw std::runtime_error("Missing index-status field.");
        }

        if (vdObj.contains("feed-update-interval"))
        {
            if (Utils::parseStrToTime(vdObj["feed-update-interval"]) == -1)
            {
                throw std::runtime_error("Invalid feed update interval.");
            }
        }

        if (vdObj.contains("offline-url"))
        {
            if (!(Utils::startsWith(vdObj["offline-url"], "file") || Utils::startsWith(vdObj["offline-url"], "http") ||
                  Utils::startsWith(vdObj["offline-url"], "https")))
            {
                throw std::runtime_error("Invalid URL provided.");
            }
        }
    }

    /**
     * @brief Validates and configures the indexer based on the provided JSON object.
     *
     * This function takes a JSON object as input, which is expected to contain configuration
     * information. It validates the JSON object to ensure it contains the required fields and has valid values,
     * based on the previous configuration of the vulnerability detection.
     * If the validation passes, no exception is thrown.
     *
     * @param configuration A constant reference to a JSON object representing the complete configuration.
     *
     * @note This function assumes that the provided JSON object follows a specific format.
     * @note If validation fails, this function throws std::runtime exception.
     */
    void validateIndexerConfiguration(const nlohmann::json& configuration) const
    {
        if (!configuration.at("indexer").contains("enabled"))
        {
            throw std::runtime_error("Missing enabled field.");
        }
        else
        {
            if (!Utils::parseStrToBool(configuration.at("indexer").at("enabled")) &&
                Utils::parseStrToBool(configuration.at("vulnerability-detection").at("enabled")) &&
                Utils::parseStrToBool(configuration.at("vulnerability-detection").at("index-status")))
            {
                throw std::runtime_error("Indexer cannot be disabled while vulnerability detection index is enabled.");
            }
        }
    }

    /**
     * @brief Validates and configures the content manager based on the provided JSON object.
     *
     * This function takes a JSON object as input, which is expected to contain configuration
     * information for the content manager. It validates the JSON object to ensure it contains the
     * required fields and has valid values. If the validation passes, no exception is thrown.
     *
     * @param configuration A constant reference to a JSON object representing the content manager configuration.
     *
     * @note This function assumes that the provided JSON object follows a specific format.
     * @note If validation fails, this function throws std::runtime exception.
     */
    void validateUpdaterConfiguration(const nlohmann::json& configuration) const
    {
        if (!configuration.contains("interval") || !configuration.at("interval").is_number())
        {
            throw std::runtime_error("Missing interval field or invalid value.");
        }

        if (!configuration.contains("ondemand") || !configuration.at("ondemand").is_boolean())
        {
            throw std::runtime_error("Missing ondemand field or invalid value.");
        }

        if (!configuration.contains("topicName") || !configuration.at("topicName").is_string())
        {
            throw std::runtime_error("Missing topicName field or invalid value.");
        }

        if (!configuration.contains("configData"))
        {
            throw std::runtime_error("Missing configData field.");
        }

        const auto& configData = configuration.at("configData");

        if (!configData.contains("consumerName") || !configData.at("consumerName").is_string())
        {
            throw std::runtime_error("Missing consumerName field or invalid value.");
        }

        if (!configData.contains("contentSource") || !configData.at("contentSource").is_string())
        {
            throw std::runtime_error("Missing contentSource field or invalid value.");
        }

        if (!configData.contains("compressionType") || !configData.at("compressionType").is_string())
        {
            throw std::runtime_error("Missing compressionType field or invalid value.");
        }

        if (!configData.contains("versionedContent") || !configData.at("versionedContent").is_string())
        {
            throw std::runtime_error("Missing versionedContent field or invalid value.");
        }

        if (!configData.contains("deleteDownloadedContent") || !configData.at("deleteDownloadedContent").is_boolean())
        {
            throw std::runtime_error("Missing deleteDownloadedContent field or invalid value.");
        }

        if (!configData.contains("outputFolder") || !configData.at("outputFolder").is_string())
        {
            throw std::runtime_error("Missing outputFolder field or invalid value.");
        }

        if (!configData.contains("contentFileName") || !configData.at("contentFileName").is_string())
        {
            throw std::runtime_error("Missing contentFileName field or invalid value.");
        }

        if (!configData.contains("databasePath") || !configData.at("databasePath").is_string())
        {
            throw std::runtime_error("Missing databasePath field or invalid value.");
        }

        if (!configData.contains("url") || !configData.at("url").is_string() ||
            (!Utils::startsWith(configData.at("url"), "http") && !Utils::startsWith(configData.at("url"), "https")))
        {
            throw std::runtime_error("Missing url field or invalid value.");
        }

        if (!configData.contains("offset") || !configData.at("offset").is_number())
        {
            throw std::runtime_error("Missing offset field or invalid value.");
        }
    }

    /**
     * @brief Validates and configures the configuration with the provided JSON object.
     * This function takes a JSON object as input, which is expected to contain configuration.
     *
     * @param configuration A constant reference to a JSON object with the configuration.
     */
    void validateAndLoadConfiguration(const nlohmann::json& configuration)
    {
        // Validate JSON
        validateConfiguration(configuration);

        // Reset configuration
        loadConfiguration(setDefaultPolicy(configuration));
    }

    /**
     * @brief Loads configuration settings from a JSON object.
     *
     * This function takes a JSON object containing configuration settings and
     * processes them for use by the policy manager's module.
     *
     * @param configuration The JSON object containing configuration settings.
     */
    void loadConfiguration(const nlohmann::json& configuration)
    {
        m_configuration = configuration;
    }

public:
    /**
     * @brief Initializes manager.
     *
     * @param configuration Manager configuration.
     */
    // LCOV_EXCL_START
    // TODO: remove the LCOV flags when the implementation of this class is completed.
    void initialize(const nlohmann::json& configuration)
    {
        // Load and validate configuration
        validateAndLoadConfiguration(configuration);

        // Initialize observer subject.
        m_subject = std::make_unique<Subject<nlohmann::json&>>();

        // Subscription to policy change events.
        m_policyChangeSubscription = std::make_unique<RouterSubscriber>("policy", "vulnerability_scanner");
        m_policyChangeSubscription->subscribe(
            [this](const std::vector<char>& message)
            {
                try
                {
                    validateAndLoadConfiguration(nlohmann::json::parse(message));
                    call(m_configuration);
                }
                catch (const std::exception& ex)
                {
                    logError(WM_VULNSCAN_LOGTAG, "Error initialization manager configuration: %s.", ex.what());
                }
            });
    }
    // LCOV_EXCL_STOP

    /**
     * @brief Teardown manager.
     *
     * @details This function is called when the manager is being destroyed.
     */
    void teardown()
    {
        m_subject.reset();
        m_policyChangeSubscription.reset();
    }

    /**
     * @brief Validates the configuration settings of the vulnerability-detection's module.
     *
     * @param configuration A constant reference to a JSON object with the configuration.
     *
     * This function checks if the configuration settings are valid and conform
     * to the expected format and values.
     *
     */

    void validateConfiguration(const nlohmann::json& configuration) const
    {
        // If the configuration is empty, throw an exception.
        if (configuration.contains("vulnerability-detection"))
        {
            // Validate vulnerability-detection configuration
            validateVulnerabilityDetectionConfiguration(configuration.at("vulnerability-detection"));
        }
        else
        {
            throw std::runtime_error("Missing vulnerability-detection field.");
        }

        // If the "indexer" configuration exists, validate it.
        // Otherwise, since it is not mandatory, the default policy will be used.
        if (configuration.contains("indexer"))
        {
            validateIndexerConfiguration(configuration);
        }

        // If the "updater" configuration exists, validate it.
        // Otherwise, since it is not mandatory, the default policy will be used.
        //
        // Note: The updater configuration is for internal use. The consumer of this module should not send the updater
        // configuration.
        if (configuration.contains("updater"))
        {
            validateUpdaterConfiguration(configuration.at("updater"));
        }

        // Check if the URL is set.
        if (!configuration.at("vulnerability-detection").contains("cti-url") && !configuration.contains("updater"))
        {
            throw std::runtime_error("Missing URL setting.");
        }
    }

    /**
     * @brief Adds subscriber.
     *
     * @param subscriber Subscriber to be added.
     */
    void addSubscriber(std::shared_ptr<Observer<nlohmann::json&>> subscriber)
    {
        m_subject->attach(std::move(subscriber));
    }

    /**
     * @brief Removes subscriber.
     *
     * @param observerId Observer ID.
     */
    void removeSubscriber(const std::string& observerId)
    {
        m_subject->detach(observerId);
    }

    /**
     * @brief Get updater configuration.
     *
     * @return nlohmann::json Configuration.
     */
    nlohmann::json getUpdaterConfiguration() const
    {
        return m_configuration.at("updater");
    }

    /**
     * @brief Get indexer connector configuration.
     *
     * @return nlohmann::json Connector configuration.
     */
    nlohmann::json getIndexerConfiguration() const
    {
        return m_configuration.at("indexer");
    }

    /**
     * @brief Get vulnerability detection configuration.
     *
     * @return nlohmann::json vulnerability-detection configuration.
     */
    nlohmann::json getVulnerabilityDetection() const
    {
        return m_configuration.at("vulnerability-detection");
    }

    /**
     * @brief Get vulnerability detection status.
     *
     * @return true if enabled or false if not.
     */
    bool isVulnerabilityDetectionEnabled() const
    {
        return Utils::parseStrToBool(m_configuration.at("vulnerability-detection").at("enabled"));
    }

    /**
     * @brief Get indexer status.
     *
     * @return true if enabled or false if not.
     */
    bool isIndexerEnabled() const
    {
        return Utils::parseStrToBool(m_configuration.at("indexer").at("enabled")) &&
               Utils::parseStrToBool(m_configuration.at("vulnerability-detection").at("index-status"));
    }

    /**
     * @brief Get feed url.
     *
     * @return std::string feedUrl.
     */
    std::string getFeedUrl() const
    {
        return m_configuration.at("vulnerability-detection").at("offline-url").get<std::string>();
    }

    /**
     * @brief Get feed url.
     *
     * @return std::string feedUrl.
     */
    long getFeedUpdateTime() const
    {
        return Utils::parseStrToTime(
            m_configuration.at("vulnerability-detection").at("feed-update-interval").get<std::string>());
    }

    /**
     * @brief Get feed url.
     *
     * @return std::string feedUrl.
     */
    std::unordered_set<std::string> getHostList() const
    {
        return m_configuration.at("indexer").at("hosts").get<std::unordered_set<std::string>>();
    }

    /**
     * @brief Get certificate authorities.
     *
     * @return std::unordered_set caList.
     */
    std::unordered_set<std::string> getCAList() const
    {
        return m_configuration.at("indexer")
            .at("ssl")
            .at("certificate_authorities")
            .get<std::unordered_set<std::string>>();
    }

    /**
     * @brief Get username.
     *
     * @return std::string username.
     */
    std::string getUsername() const
    {
        return m_configuration.at("indexer").at("username").get<std::string>();
    }

    /**
     * @brief Get password.
     *
     * @return std::string password.
     */
    std::string getPassword() const
    {
        return m_configuration.at("indexer").at("password").get<std::string>();
    }

    /**
     * @brief Get certificate.
     *
     * @return std::string certificate.
     */
    std::string getCertificate() const
    {
        return m_configuration.at("indexer").at("ssl").at("certificate").get<std::string>();
    }

    /**
     * @brief Get key.
     *
     * @return std::string key.
     */
    std::string getKey() const
    {
        return m_configuration.at("indexer").at("ssl").at("key").get<std::string>();
    }

    /**
     * @brief Get CTI url.
     * @return std::string CTI-Server url.
     */
    std::string getCTIUrl() const
    {
        return m_configuration.at("updater").at("configData").at("url").get_ref<const std::string&>();
    }

    /**
     * @brief Get translation LRU size.
     *
     * @return uint32_t translation LRU size.
     */
    uint32_t getTranslationLRUSize() const
    {
        return m_configuration.at("translationLRUSize").get<uint32_t>();
    }

    /**
     * @brief Get osdata LRU size.
     *
     * @return uint32_t osdata LRU size.
     */
    uint32_t getOsdataLRUSize() const
    {
        return m_configuration.at("osdataLRUSize").get<uint32_t>();
    }

    /**
     * @brief Get remediation LRU size.
     *
     * @return uint32_t remediation LRU size.
     */
    uint32_t getRemediationLRUSize() const
    {
        return m_configuration.at("remediationLRUSize").get<uint32_t>();
    }

    /**
     * @brief Retrieves the current status of the manager's scan.
     *
     * This function retrieves the current status of the manager's scan
     * from the configuration and returns it as a ManagerScanStatus enum value.
     *
     * @return true if the manager's scan is disabled, false otherwise.
     */
    bool getManagerDisabledScan() const
    {
        return m_configuration.at("managerDisabledScan").get<uint32_t>() == MANAGER_SCAN_DISABLED;
    }

    /**
     * @brief Retrieves the name of the manager node for vulnerability detection.
     *
     * This function retrieves the name of the manager node for vulnerability detection
     * from the configuration and returns it as a std::string.
     *
     * @return std::string The name of the manager node for vulnerability detection.
     */
    std::string getClusterNodeName() const
    {
        return m_configuration.at("clusterNodeName").get_ref<const std::string&>();
    }

    /**
     * @brief Get status of the cluster.
     * This function retrieves the cluster status from the configuration and returns it as a bool
     * @return bool cluster status.
     */
    bool getClusterStatus() const
    {
        return m_configuration.at("clusterEnabled").get<bool>();
    }

    /**
     * @brief Get cluster name.
     * This function retrieves the cluster name from the configuration and returns it as a std::string
     * @return std::string cluster name.
     */
    std::string getClusterName() const
    {
        return m_configuration.at("clusterName").get_ref<const std::string&>();
    }

    /**
     * @brief Retrieves the alerts max events per second.
     * This function retrieves the alerts max events per second from the configuration and returns it as a uint32_t.
     * @return uint32_t The alerts max events per second.
     */
    uint32_t getAlertsMaxEventsPerSecond() const
    {
        if (m_configuration.contains("wmMaxEps") && m_configuration.at("wmMaxEps").is_number())
        {
            return m_configuration.at("wmMaxEps").get<uint32_t>();
        }
        return 0;
    }
};

#endif //_POLICY_MANAGER_HPP
